<?php
/**
 * blXslt
 *
 * @version: 2.2
 * @date: 2008-12-04
 * @author: Vladimir Romanovich <ibnteo@gmail.com>
 * @license: GPL2
 */

// запустить стартовый сценарий
blXslt::main();
exit();

/**
 * Класс blXslt
 */
class blXslt
{
    // максимальный уровень рекурсии
    const MAX_RECURSION = 10000;

    // данные для подключения к базам данных
    public $config = array();

    // уровень рекурсии
    protected $level = 0;

    // разрешено выводить результат
    protected $is_output = TRUE;

    // обрабатываемый документ
    public $doc;

    // входной документ
    protected $doc_in;

    // таблица стилей
    protected $xslt;

    // текущий путь
    public $path = '';

    // очищенные от "магических" кавычек request параметры
    protected $request = array();

    // DTD определение для сущностей    
    protected $entities;

    // текущая инструкция процессора
    public $element_pi;
    // следующий (или предыдущий если element="previous") элемент от текущей инструкции процессора
    public $element_pi_next;
    // элемент с параметрами инструкции процессора
    public $attributes_pi;

    /**
     * Конструктор
     */
    public function __construct($level = 0)
    {
        // не допустить бесконечную рекурсию
        if ($level > self::MAX_RECURSION)
        {
            throw new Exception('Maximum recursion level of '.MAX_RECURSION.' reached.');
        }
        $this->level = $level;
    }

    /**
     * Стартовый сценарий
     */
    public static function main()
    {
        // создать экземпляр класса
        $bl_xslt = new blXslt();

        // запрашиваемый документ (XML или XSLT)
        $doc = new DomDocument();
        #$doc_file = (isset($_SERVER['argc']) && $_SERVER['argc'] > 1) ? $_SERVER['argv'][1] : substr($_SERVER['REDIRECT_URL'], 1);
        $script_name = split('/', $_SERVER['SCRIPT_NAME']);
        $script_name = array_slice($script_name, 0, sizeof($script_name) - 1);
        $script_dir = join('/', $script_name).'/';
        
        $doc_file = substr($_SERVER['REDIRECT_URL'], strlen($script_dir));
        $doc->load($doc_file, LIBXML_DTDLOAD + LIBXML_NOENT);

        $bl_xslt->path = $doc_file ? dirname($doc_file) . '/' : '';

        if ($doc->documentElement->namespaceURI === 'http://www.w3.org/1999/XSL/Transform')
        {
            // документ является XSLT файлом
            $bl_xslt->doc = new DomDocument();
            $bl_xslt->doc->loadXml('<request><?xbl-request?></request>');
            $bl_xslt->processing_instruction();
            $bl_xslt->xslt = $doc;
            $bl_xslt->transform();
        }
        else
        {
            // документ является XML файлом
            $bl_xslt->doc = $doc;
            $bl_xslt->processing_instruction();
        }
    }

    /**
     * Выполнить XSLT трансформацию
     */
    protected function transform()
    {
        $proc = new XsltProcessor();
        $proc->importStylesheet($this->xslt);
        $this->doc_in = $this->doc;
        $this->doc = $proc->transformToDoc($this->doc);

        // обработать результат трансформации
        $this->processing_instruction();
    }

    /**
     * Обработать результат трансформации
     */
    protected function processing_instruction()
    {
        $xpath_node = new DomXpath($this->doc);

        // перебор инструкций процессора в обратном порядке, только по первым на одном уровне
        $node_pi = $xpath_node->query('//processing-instruction()[not(preceding-sibling::processing-instruction())]');
        for ($index_node = $node_pi->length - 1; $index_node >= 0; $index_node --)
        {
            $xpath_pi = new DomXpath($this->doc);

            // перебор инструкций одного уровня в прямом порядке
            $pi = $xpath_pi->query('. | following-sibling::processing-instruction()', $node_pi->item($index_node));
            foreach ($xpath_pi->query('. | following-sibling::processing-instruction()', $node_pi->item($index_node)) as $element_pi)
            {
                $this->element_pi = $element_pi;
                $pi_name = $this->element_pi->nodeName;

                // обрабатываем только свои инструкции процессора
                if (substr($pi_name, 0, 4) === 'xbl-')
                {
                    $method = str_replace('-', '_', $pi_name);

                    if (method_exists($this, $method))
                    {
                        // вызвать функцию
                        $this->$method();
                    }
                    else
                    {
                        // вызвать внешнюю функцию
                        $method_split = split('_', $method);
                        $class_name = 'blXslt'.ucfirst($method_split[1]);
                        include_once($class_name.'.php');
                        $obj = new $class_name($this);
                        $obj->$method();
                    }

                    // убрать исполненную инструкцию процессора
                    $this->element_pi->parentNode->removeChild($this->element_pi);
                }
            }
        }
    }

    /**
     * Сформировать документ с параметрами инструкции процессора
     */
    public function set_document_pi()
    {
        $document_pi = new DomDocument();
        $document_pi->loadXml('<pi '.$this->element_pi->nodeValue.'/>');
        $this->attributes_pi = $document_pi->documentElement;
        $this->element_pi_next =
        (
            $this->attributes_pi->getAttribute('element') === 'previous'
            ? $this->element_pi->previousSibling
            : $this->element_pi->nextSibling
        );
    }

    /**
     * Полный путь к файлу с учетом текущей директории
     */
    public function get_path($href, $absolute = FALSE)
    {
        if ( ! $absolute)
        {
            if (substr($href, 0, 1) === '/')
            {
                $href = substr($href, 1);
            } else {
                $href = $this->path.$href;
            }
        }
        return $href;
    }

    /**
     * Вывести HTTP заголовки
     */
    public function set_header()
    {
        foreach ($this->attributes_pi->attributes as $key => $value)
        {
            switch (strtolower($key))
            {
                case 'content-type':
                    header('Content-Type: '.$value->value);
                    break;
                case 'expires':
                    header('Expires: '.gmdate('D, d M Y H:i:s', time()+$value->value).' GMT');
                    break;
                case 'last-modified':
                    header('Last-Modified: '.gmdate('D, d M Y H:i:s', strtotime($value->value)).' GMT');
                    break;
                case 'status':
                    header('HTTP/1.1 '.$value->value);
                    break;
                default:
                    if (strtolower(substr($key, 0, 7)) === 'header-')
                    {
                        header(substr($key, 7).': '.$value->value);
                    }
            }
        }
    }

    /**
     * Вывести документ или результат трансформации документа
     */
    public function xbl_response()
    {
        if ($this->is_output)
        {
            $this->set_document_pi();
            $this->set_header();

            // документ для трансформации или вывода
            $doc = new DomDocument('1.0', 'utf-8');
            $doc->appendChild($doc->importNode($this->element_pi_next, TRUE));

            $xslt_file = $this->attributes_pi->getAttribute('stylesheet');
            if ($xslt_file !== '')
            {
                // трасформировать в выходной поток
                $doc_xslt = new DomDocument();
                $doc_xslt->load($this->get_path($xslt_file), LIBXML_DTDLOAD + LIBXML_NOENT);
                $proc = new XsltProcessor();
                $xslt = $proc->importStylesheet($doc_xslt);
                $proc->transformToURI($doc, 'php://output');
            } else {
                // выдать XML
                $this->doc->encoding = 'utf-8';
                $this->doc->formatOutput = true;
                echo ($doc->saveXml());
            }
        }
    }

    /**
     * Установить Cookie
     */
    public function xbl_response_cookie()
    {
        $this->set_document_pi();

        $name = $this->element_pi_next->getAttribute('name');
        $value = $this->element_pi_next->getAttribute('value');
        $expire = $this->element_pi_next->getAttribute('expire');
        $path = $this->element_pi_next->getAttribute('path');
        $domain = $this->element_pi_next->getAttribute('domain');
        $secure = ($this->element_pi_next->getAttribute('secure') === 'yes');
        setcookie($name, $value, ($expire ? time() + $expire : null), $path, $domain, $secure);
    }

    /**
     * Сохранить документ в файл
     */
    public function xbl_save()
    {
        $this->set_document_pi();

        // документ для трансформации или вывода
        $doc = new DomDocument('1.0', 'utf-8');
        $doc->appendChild($doc->importNode($this->element_pi_next, TRUE));

        $xslt_file = $this->attributes_pi->getAttribute('stylesheet');
        $file_to = $this->get_path($this->attributes_pi->getAttribute('path'));
        $is_file = file_exists($file_to);
        if (! is_dir(dirname($file_to)))
        {
            // создать директорию, если ее нет
            mkdir(dirname($file_to), 0777, TRUE);
        }
        if ($xslt_file !== '')
        {
            // произвести XSLT трансформацию во временный файл
            $doc_xslt = new DomDocument();
            $doc_xslt->load($this->get_path($xslt_file, $this->current_dir), LIBXML_DTDLOAD + LIBXML_NOENT);
            $proc = new XsltProcessor();
            $xslt = $proc->importStylesheet($doc_xslt);
            $proc->transformToURI($doc, $file_to.'.tmp.');
        } else {
            // сохранить XML документ во временный файл
            $doc->save($file_to.'.tmp.');
        }
        // заменить исходный файл из временного
        rename($file_to.'.tmp.', $file_to);
        if (! $is_file)
        {
            // сменить права доступа для возможности записи всех
            $chmod_result = chmod($file_to, 0666);
        }
    }

    /**
     * Выполнение произвольного PHP скрипта
     */
    public function xbl_php()
    {
        $php_result = eval($this->element_pi->nodeValue);
        if (is_array($php_result))
        {
            $php_node = $this->doc->createElement('php');
            blXslt::array_xml($php_result, $php_node);
        }
        else
        {
            $php_node = $this->doc->createTextNode($php_result);
        }
        $this->element_pi->parentNode->insertBefore($php_node, $this->element_pi);
    }

    /**
     * Произвести трансформацию и обработать инструкции процессора
     * !!! замена старого xbl-transfer !!!
     */
    public function xbl_stylesheet()
    {
        $this->set_document_pi();

        $doc = new DomDocument();
        $doc->appendChild($doc->importNode($this->element_pi_next, TRUE));

        $bl_xslt = new blXslt($this->level + 1);
        $bl_xslt->request = $this->request;
        $bl_xslt->entities = $this->entities;
        $bl_xslt->doc = $doc;

        $href = $this->attributes_pi->getAttribute('href');
        if ($href)
        {
            $bl_xslt->xslt = new DomDocument();
            $bl_xslt->xslt->load($this->get_path($href), LIBXML_DTDLOAD + LIBXML_NOENT);
            $bl_xslt->path = (substr($href, 0, 1) === '/' ? '' : dirname($this->get_path($href)) . '/');
        }
        else
        {
            $bl_xslt->xslt = $this->xslt;
        }

        $bl_xslt->transform();
    }

    /**
     * Поместить в документ результат трансформации
     */
    public function xbl_transform()
    {
        $this->set_document_pi();

        $doc = new DomDocument();
        $doc->appendChild($doc->importNode($this->element_pi_next, TRUE));

        $bl_xslt = new blXslt($this->level + 1);
        $bl_xslt->is_output = FALSE;
        $bl_xslt->request = $this->request;
        $bl_xslt->entities = $this->entities;
        $bl_xslt->doc = $doc;

        $href = $this->attributes_pi->getAttribute('href');
        if ($href)
        {
            $bl_xslt->xslt = new DomDocument();
            $bl_xslt->xslt->load($this->get_path($href), LIBXML_DTDLOAD + LIBXML_NOENT);
            $bl_xslt->path = (substr($href, 0, 1) === '/' ? '' : dirname($this->get_path($href)) . '/');
        }
        else
        {
            $bl_xslt->xslt = $this->xslt;
        }

        $bl_xslt->transform();

        $node_import = $this->doc->importNode($bl_xslt->doc->documentElement, true);
        $this->element_pi_next->parentNode->replaceChild($node_import, $this->element_pi_next);
    }

    /**
     * Значения всех переданных параметров (GET, POST и др.)
     */
    public function xbl_request()
    {
        if ($_SERVER['QUERY_STRING'])
        {
            // разобрать query_string
            $this->request_content('get', $_SERVER['QUERY_STRING']);
        }
        $content_type = explode(';', $_SERVER['CONTENT_TYPE']);
        if ($content_type[0] === 'multipart/form-data')
        {
            // сформировать данные в случае enctype=multipart/form-data
            $this->request_array(strtolower($_SERVER['REQUEST_METHOD']), $_POST);
            $this->request_array('files', $_FILES);
        }
        else
        {
            if (strtolower($_SERVER['REQUEST_METHOD']) !== 'get')
            {
                // разобрать входные данные на параметры
                $this->request_content(strtolower($_SERVER['REQUEST_METHOD']), file_get_contents('php://input'));
            }
        }
    }

    /**
     * Значения COOKIE параметров
     */
    public function xbl_request_cookie()
    {
        $this->request_array('cookie', $_COOKIE);
    }

    /**
     * Значения SERVER параметров
     */
    public function xbl_request_server()
    {
        $this->request_array('server', $_SERVER);
    }

    /**
     * Убрать "магические" кавычки
     */
    public static function stripslashes_array($value)
    {
        if (is_array($value))
        {
            $value = array_map('blXslt::stripslashes_array', $value);
        }
        elseif (( ! empty($value)) && is_string($value))
        {
            $value = stripslashes($value);
        }
        return $value;
    }

    /**
     * Рекурсивная функция преобразования массива в Xml
     */
    public static function array_xml($array, $node, $is_lower = FALSE)
    {
        foreach ($array as $key => $value)
        {
            if ($is_lower)
            {
                // имена параметров в нижнем регистре
                $key = strtolower($key);
            }
            if ($key === 'xml')
            {
                // создать XML узел из текста
                blXslt::text_xml($value, $node);
            }
            elseif ($key === 'xhtml')
            {
                // создать XHTML узел из текста
                blXslt::text_xml($value, $node, TRUE);
            }
            else
            {
                try
                {
                    // создать узел
                    $node_key = $node->ownerDocument->createElement(is_string($key) ? $key : 'item');
                }
                catch (Exception $exception)
                {
                    // при неправильном имени создать узел с именем параметра в атрибуте
                    $node_key = $node->ownerDocument->createElement('param');
                    $node_key->setAttribute('name', $key);
                }
                $node->appendChild($node_key);
                if (is_array($value))
                {
                    // рекурсивная обработка массива
                    blXslt::array_xml($value, $node_key);
                }
                else
                {
                    // создать текстовый узел
                    $node_value = $node->ownerDocument->createTextNode($value);
                    $node_key->appendChild($node_value);
                }
            }
        }
    }
    
    /**
     * Функция преобразования текста в XML документ
     */
    public static function text_xml($value, $node, $is_xhtml = FALSE)
    {
        // создать документ с корневым элементом из текста
        $doc_xml = new DomDocument();
        if ( ! isset($this->entities))
        {
            $xml = file_exists('entities.dtd') ? '<!DOCTYPE xsl:stylesheet SYSTEM "entities.dtd">' : '';
        }
        $xml = $this->entites;
        if ($is_xhtml)
        {
	        $xml .= '<html xmlns="http://www.w3.org/1999/xhtml">'.$value.'</html>';
        }
        else
        {
	        $xml .= '<root>'.$value.'</root>';
        }
        $doc_xml->loadXml($xml, LIBXML_DTDLOAD + LIBXML_NOENT);

        // вставить вложенные узлы в исходный документ
        foreach ($doc_xml->documentElement->childNodes as $item)
        {
            // Импортировать узел в узел целевого документа
            $node_doc = $node->ownerDocument->importNode($item, TRUE);
            $node->appendChild($node_doc);
        }
    }

    /**
     * Преобразовать массив параметров в элемент
     */
    protected function request_array($name, $array)
    {
        if (get_magic_quotes_gpc())
        {
            // убрать "магические" кавычки
            $array = blXslt::stripslashes_array($array);
        }
        $node = $this->doc->createElement($name);
        // преобразовать данные из массива в xml
        blXslt::array_xml($array, $node, TRUE);
        $this->element_pi->parentNode->insertBefore($node, $this->element_pi);
    }

    /**
     * Считывание данных при других REQUEST_METHOD напрямую
     */
    protected function request_content($name, $input)
    {
        $request_node = $this->doc->createElement($name);
        if ($input)
        {
            // разобрать параметры
            foreach (explode('&', $input) as $param)
            {
                // отделить имя от значения
                $keyvalue = explode('=', $param, 2);
                try
                {
                    // создать узел с именем параметра
                    $node = $this->doc->createElement(urldecode($keyvalue[0]));
                }
                catch (Exception $e)
                {
                    // в случае невалидного имени узла создать узел <param>
                    $node = $this->doc->createElement('param');
                    $node->setAttribute('name', urldecode($keyvalue[0]));
                }
                // значение параметра
                $text = $this->doc->createTextNode(urldecode($keyvalue[1]));
                $node->appendChild($text);
                $request_node->appendChild($node);
            }
        }
        $this->element_pi->parentNode->insertBefore($request_node, $this->element_pi);
    }

} // Конец blXslt
